﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SeleniumWebObject.cs" company="">
// </copyright>
// <summary>
//   The selenium web object.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace UITest.Selenium.SeleniumControls
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Threading;

    using OpenQA.Selenium;
    using OpenQA.Selenium.Interactions;

    using UITest.Report;
    using UITest.Selenium.SeleniumContext;

    /// <summary>
    ///     The selenium web object.
    /// </summary>
    public class SeleniumWebObject : ISeleniumWebObject
    {
        #region Fields

        /// <summary>
        ///     The find by.
        /// </summary>
        protected readonly By FindBy;

        /// <summary>
        ///     The selenium context.
        /// </summary>
        protected SeleniumContext SeleniumContext;

        /// <summary>
        ///     The web element.
        /// </summary>
        protected IWebElement WebElement;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="SeleniumWebObject"/> class.
        /// </summary>
        /// <param name="seleniumContext">
        /// The selenium context.
        /// </param>
        /// <param name="findBy">
        /// The find by.
        /// </param>
        public SeleniumWebObject(SeleniumContext seleniumContext, By findBy)
        {
            this.FindBy = findBy;
            this.SeleniumContext = seleniumContext;
            this.WebElement = null;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SeleniumWebObject"/> class.
        /// </summary>
        /// <param name="seleniumContext">
        /// The selenium context.
        /// </param>
        /// <param name="webElement">
        /// The web element.
        /// </param>
        public SeleniumWebObject(SeleniumContext seleniumContext, IWebElement webElement)
        {
            this.WebElement = webElement;
            this.SeleniumContext = seleniumContext;
        }

        #endregion

        #region Public Properties

        /// <summary>
        ///     Gets a value indicating whether checked.
        /// </summary>
        public bool Checked
        {
            get
            {
                return this.ElementExists()
                       && (bool)
                          this.JavaScriptExecutor.ExecuteScript("return arguments[0].checked;", this.FindElement());
            }
        }

        /// <summary>
        ///     Gets a value indicating whether displayed.
        /// </summary>
        public bool Displayed
        {
            get
            {
                try
                {
                    return this.ElementExists() && this.FindElement().Displayed;
                }
                catch (Exception)
                {
                    return false;
                }
            }
        }

        /// <summary>
        ///     Gets a value indicating whether enabled.
        /// </summary>
        public bool Enabled
        {
            get
            {
                try
                {
                    return this.ElementExists() && this.FindElement().Enabled;
                }
                catch (Exception)
                {
                    return false;
                }
            }
        }

        /// <summary>
        ///     Gets a value indicating whether exists.
        /// </summary>
        public bool Exists
        {
            get
            {
                return this.ElementExists();
            }
        }

        /// <summary>
        ///     Gets or sets the name.
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Gets a value indicating whether read only.
        /// </summary>
        public bool ReadOnly
        {
            get
            {
                return this.ElementExists()
                       && (bool)
                          this.JavaScriptExecutor.ExecuteScript("return arguments[0].checked;", this.FindElement());
            }
        }

        /// <summary>
        ///     Gets or sets the remark.
        /// </summary>
        public string Remark { get; set; }

        /// <summary>
        ///     Gets the text.
        /// </summary>
        public virtual string Text
        {
            get
            {
                return this.FindElement().Text;
            }
        }

        #endregion

        #region Properties

        /// <summary>
        ///     Gets the java script executor.
        /// </summary>
        protected IJavaScriptExecutor JavaScriptExecutor
        {
            get
            {
                return this.SeleniumContext.WebDriver;
            }
        }

        /// <summary>
        ///     The report.
        /// </summary>
        protected ReportListener Report
        {
            get
            {
                return ReportFactory.GetReportListener();
            }
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// The click.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        /// <param name="delay">
        /// The delay.
        /// </param>
        public virtual void Click(string message = "", int timeout = -1, int delay = 500)
        {
            this.SeleniumContext.WaitUntil(isTrue => (this.Displayed && this.Enabled), message, timeout);
            this.Report.WriteTestOutput("Click on " + this.FindElement().TagName);
            this.SeleniumContext.WaitUntil(isTrue => this.TryToClick(), message, timeout);
        }

        /// <summary>
        /// The double click.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        /// <param name="delay">
        /// The delay.
        /// </param>
        public virtual void DoubleClick(string message = "", int timeout = -1, int delay = 500)
        {
            this.SeleniumContext.WaitUntil(isTrue => (this.Displayed && this.Enabled), message, timeout);
            this.Report.WriteTestOutput("Double Click on " + this.FindElement().TagName);
            this.SeleniumContext.WaitUntil(isTrue => this.TryToDoubleClick(), message, timeout);
        }

        /// <summary>
        /// The find child element.
        /// </summary>
        /// <param name="findBy">
        /// The find by.
        /// </param>
        /// <returns>
        /// The <see cref="IWebElement"/>.
        /// </returns>
        public IWebElement FindChildElement(By findBy)
        {
            IWebElement parentElement = this.FindElement();
            IWebElement element = parentElement.FindElement(findBy);

            return element;
        }

        /// <summary>
        /// The find child elements.
        /// </summary>
        /// <param name="findBy">
        /// The find by.
        /// </param>
        /// <returns>
        /// The <see cref="ReadOnlyCollection"/>.
        /// </returns>
        public ReadOnlyCollection<IWebElement> FindChildElements(By findBy)
        {
            IWebElement parentElement = this.FindElement();
            ReadOnlyCollection<IWebElement> elements = parentElement.FindElements(findBy);

            return elements;
        }

        /// <summary>
        ///     The find element.
        /// </summary>
        /// <returns>
        ///     The <see cref="IWebElement" />.
        /// </returns>
        public IWebElement FindElement()
        {
            return this.WebElement ?? this.FindElement(this.FindBy);
        }

        /// <summary>
        /// The find element.
        /// </summary>
        /// <param name="findBy">
        /// The find by.
        /// </param>
        /// <returns>
        /// The <see cref="IWebElement"/>.
        /// </returns>
        public IWebElement FindElement(By findBy)
        {
            return this.WebElement ?? this.SeleniumContext.WebDriver.FindElement(findBy);
        }

        /// <summary>
        ///     The find elements.
        /// </summary>
        /// <returns>
        ///     The <see cref="ReadOnlyCollection" />.
        /// </returns>
        public ReadOnlyCollection<IWebElement> FindElements()
        {
            if (this.FindBy != null)
            {
                return this.SeleniumContext.WebDriver.FindElements(this.FindBy);
            }

            return new ReadOnlyCollection<IWebElement>(new List<IWebElement> { this.WebElement });
        }

        /// <summary>
        /// The get parent element.
        /// </summary>
        /// <returns>
        /// The <see cref="IWebElement"/>.
        /// </returns>
        public IWebElement GetParentElement()
        {
            return
                (IWebElement)
                this.JavaScriptExecutor.ExecuteScript("return arguments[0].parentNode;", this.FindElement());
        }

        /// <summary>
        /// The get selenium context.
        /// </summary>
        /// <returns>
        /// The <see cref="SeleniumContext"/>.
        /// </returns>
        public SeleniumContext GetSeleniumContext()
        {
            return this.SeleniumContext;
        }

        /// <summary>
        /// The get web element.
        /// </summary>
        /// <returns>
        /// The <see cref="IWebElement"/>.
        /// </returns>
        public virtual IWebElement GetWebElement()
        {
            return this.FindElement();
        }

        /// <summary>
        /// The hover.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        public virtual void Hover(string message = "", int timeout = -1)
        {
            this.SeleniumContext.WaitUntil(isTrue => this.TryToHover(), message, timeout);
        }

        /// <summary>
        /// The js click.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        public virtual void JSClick(string message = "", int timeout = -1)
        {
            this.SeleniumContext.WaitUntil(isTrue => this.TryToJSClick(), message, timeout);
        }

        /// <summary>
        /// The set attribute.
        /// </summary>
        /// <param name="attribute">
        /// The attribute.
        /// </param>
        /// <param name="value">
        /// The value.
        /// </param>
        public void SetAttribute(string attribute, string value)
        {
            this.JavaScriptExecutor.ExecuteScript(
                string.Format("arguments[0].setAttribute('{0}', '{1}');", attribute, value), 
                this.FindElement());
        }

        /// <summary>
        ///     The to derived.
        /// </summary>
        /// <typeparam name="T">
        /// </typeparam>
        /// <returns>
        ///     The <see cref="T" />.
        /// </returns>
        public T ToDerived<T>() where T : SeleniumWebObject
        {
            return (T)Activator.CreateInstance(typeof(T), this.SeleniumContext, this.WebElement);
        }

        /// <summary>
        /// The wait for control ready.
        /// </summary>
        public void WaitForControlReady()
        {
            this.WaitUntil(x => this.Displayed);
        }

        /// <summary>
        /// The wait until.
        /// </summary>
        /// <param name="isTrue">
        /// The is true.
        /// </param>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="timeout">
        /// The timeout.
        /// </param>
        /// <param name="throwException">
        /// The throw exception.
        /// </param>
        /// <returns>
        /// The <see cref="bool"/>.
        /// </returns>
        public bool WaitUntil(
            Func<IWebDriver, bool> isTrue, 
            string message = "", 
            int timeout = -1, 
            bool throwException = false)
        {
            return this.SeleniumContext.WaitUntil(isTrue, message, timeout, throwException);
        }

        /// <summary>
        /// The write test output.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        public virtual void WriteTestOutput(string message)
        {
            this.Report.WriteTestOutput(message + this.FindElement().GetAttribute("ClassName"));
        }

        #endregion

        #region Methods

        private bool ElementExists()
        {
            ReadOnlyCollection<IWebElement> webElements = this.FindElements();
            return webElements.Count >= 1;
        }

        private bool TryToClick(int wait = 500)
        {
            bool isClicked = false;
            try
            {
                IWebElement element = this.FindElement();
                if (element.Displayed && element.Enabled)
                {
                    var action = new Actions(this.SeleniumContext.WebDriver);
                    action.MoveToElement(element);
                    action.Click();
                    action.Perform();
                    Thread.Sleep(wait);
                    isClicked = true;
                }
            }
            catch (Exception ex)
            {
                this.Report.WriteTestOutput(ex.ToString());
            }

            return isClicked;
        }

        private bool TryToDoubleClick(int wait = 500)
        {
            bool isClicked = false;
            try
            {
                IWebElement element = this.FindElement();
                if (element.Displayed && element.Enabled)
                {
                    var action = new Actions(this.SeleniumContext.WebDriver);
                    action.MoveToElement(element);
                    action.DoubleClick();
                    action.Perform();
                    Thread.Sleep(wait);

                    isClicked = true;
                }
            }
            catch (Exception ex)
            {
                this.Report.WriteTestOutput(ex.ToString());
            }

            return isClicked;
        }

        private bool TryToHover()
        {
            bool isHovered = false;
            try
            {
                IWebElement element = this.FindElement();
                if (element.Displayed && element.Enabled)
                {
                    var action = new Actions(this.SeleniumContext.WebDriver);
                    action.MoveToElement(element);
                    action.Perform();
                    isHovered = true;
                }
            }
            catch (Exception ex)
            {
                this.Report.WriteTestOutput(ex.ToString());
            }

            return isHovered;
        }

        private bool TryToJSClick()
        {
            bool isClicked = false;
            try
            {
                IWebElement element = this.FindElement();
                if (element.Displayed && element.Enabled)
                {
                    this.JavaScriptExecutor.ExecuteScript("arguments[0].click();", this.FindElement());
                    isClicked = true;
                }
            }
            catch (Exception ex)
            {
                this.Report.WriteTestOutput(ex.ToString());
            }

            return isClicked;
        }

        #endregion
    }
}